diff --git a/doc/api/fs.md b/doc/api/fs.md
index ebab0a600735ad..6ac840a76b9600 100644
--- a/doc/api/fs.md
+++ b/doc/api/fs.md
@@ -2368,6 +2368,38 @@ changes:
 
 Synchronous lchown(2). Returns `undefined`.
 
+## `fs.lutimes(path, atime, mtime, callback)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `path` {string|Buffer|URL}
+* `atime` {number|string|Date}
+* `mtime` {number|string|Date}
+* `callback` {Function}
+  * `err` {Error}
+
+Changes the access and modification times of a file in the same way as
+[`fs.utimes()`][], with the difference that if the path refers to a symbolic
+link, then the link is not dereferenced: instead, the timestamps of the
+symbolic link itself are changed.
+
+No arguments other than a possible exception are given to the completion
+callback.
+
+## `fs.lutimesSync(path, atime, mtime)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `path` {string|Buffer|URL}
+* `atime` {number|string|Date}
+* `mtime` {number|string|Date}
+
+Change the file system timestamps of the symbolic link referenced by `path`.
+Returns `undefined`, or throws an exception when parameters are incorrect or
+the operation fails. This is the synchronous version of [`fs.lutimes()`][].
+
 ## `fs.link(existingPath, newPath, callback)`
 <!-- YAML
 added: v0.1.31
@@ -4946,6 +4978,23 @@ changes:
 Changes the ownership on a symbolic link then resolves the `Promise` with
 no arguments upon success.
 
+### `fsPromises.lutimes(path, atime, mtime)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `path` {string|Buffer|URL}
+* `atime` {number|string|Date}
+* `mtime` {number|string|Date}
+* Returns: {Promise}
+
+Changes the access and modification times of a file in the same way as
+[`fsPromises.utimes()`][], with the difference that if the path refers to a
+symbolic link, then the link is not dereferenced: instead, the timestamps of
+the symbolic link itself are changed.
+
+Upon success, the `Promise` is resolved without arguments.
+
 ### `fsPromises.link(existingPath, newPath)`
 <!-- YAML
 added: v10.0.0
@@ -5755,6 +5804,7 @@ the file contents.
 [`fs.ftruncate()`]: #fs_fs_ftruncate_fd_len_callback
 [`fs.futimes()`]: #fs_fs_futimes_fd_atime_mtime_callback
 [`fs.lstat()`]: #fs_fs_lstat_path_options_callback
+[`fs.lutimes()`]: #fs_fs_lutimes_path_atime_mtime_callback
 [`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
 [`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
 [`fs.open()`]: #fs_fs_open_path_flags_mode_callback
@@ -5778,6 +5828,7 @@ the file contents.
 [`fs.writev()`]: #fs_fs_writev_fd_buffers_position_callback
 [`fsPromises.open()`]: #fs_fspromises_open_path_flags_mode
 [`fsPromises.opendir()`]: #fs_fspromises_opendir_path_options
+[`fsPromises.utimes()`]: #fs_fspromises_utimes_path_atime_mtime
 [`inotify(7)`]: http://man7.org/linux/man-pages/man7/inotify.7.html
 [`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
 [`net.Socket`]: net.html#net_class_net_socket
diff --git a/lib/fs.js b/lib/fs.js
index 9b70b237ef00e1..03b5451c4c1123 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -1303,6 +1303,28 @@ function futimesSync(fd, atime, mtime) {
   handleErrorFromBinding(ctx);
 }
 
+function lutimes(path, atime, mtime, callback) {
+  callback = makeCallback(callback);
+  path = getValidatedPath(path);
+
+  const req = new FSReqCallback();
+  req.oncomplete = callback;
+  binding.lutimes(pathModule.toNamespacedPath(path),
+                  toUnixTimestamp(atime),
+                  toUnixTimestamp(mtime),
+                  req);
+}
+
+function lutimesSync(path, atime, mtime) {
+  path = getValidatedPath(path);
+  const ctx = { path };
+  binding.lutimes(pathModule.toNamespacedPath(path),
+                  toUnixTimestamp(atime),
+                  toUnixTimestamp(mtime),
+                  undefined, ctx);
+  handleErrorFromBinding(ctx);
+}
+
 function writeAll(fd, isUserFd, buffer, offset, length, callback) {
   // write(fd, buffer, offset, length, position, callback)
   fs.write(fd, buffer, offset, length, null, (writeErr, written) => {
@@ -1938,6 +1960,8 @@ module.exports = fs = {
   linkSync,
   lstat,
   lstatSync,
+  lutimes,
+  lutimesSync,
   mkdir,
   mkdirSync,
   mkdtemp,
diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index 31eaeef2846216..515ac20dfa5b8c 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -474,6 +474,14 @@ async function futimes(handle, atime, mtime) {
   return binding.futimes(handle.fd, atime, mtime, kUsePromises);
 }
 
+async function lutimes(path, atime, mtime) {
+  path = getValidatedPath(path);
+  return binding.lutimes(pathModule.toNamespacedPath(path),
+                         toUnixTimestamp(atime),
+                         toUnixTimestamp(mtime),
+                         kUsePromises);
+}
+
 async function realpath(path, options) {
   options = getOptions(options, {});
   path = getValidatedPath(path);
@@ -541,6 +549,7 @@ module.exports = {
     lchown,
     chown,
     utimes,
+    lutimes,
     realpath,
     mkdtemp,
     writeFile,
diff --git a/src/node_file.cc b/src/node_file.cc
index 0cce86e8f36dc3..ecf49dead7cdaa 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -2247,6 +2247,35 @@ static void FUTimes(const FunctionCallbackInfo<Value>& args) {
   }
 }
 
+static void LUTimes(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+
+  const int argc = args.Length();
+  CHECK_GE(argc, 3);
+
+  BufferValue path(env->isolate(), args[0]);
+  CHECK_NOT_NULL(*path);
+
+  CHECK(args[1]->IsNumber());
+  const double atime = args[1].As<Number>()->Value();
+
+  CHECK(args[2]->IsNumber());
+  const double mtime = args[2].As<Number>()->Value();
+
+  FSReqBase* req_wrap_async = GetReqWrap(env, args[3]);
+  if (req_wrap_async != nullptr) {  // lutimes(path, atime, mtime, req)
+    AsyncCall(env, req_wrap_async, args, "lutime", UTF8, AfterNoArgs,
+              uv_fs_lutime, *path, atime, mtime);
+  } else {  // lutimes(path, atime, mtime, undefined, ctx)
+    CHECK_EQ(argc, 5);
+    FSReqWrapSync req_wrap_sync;
+    FS_SYNC_TRACE_BEGIN(lutimes);
+    SyncCall(env, args[4], &req_wrap_sync, "lutime",
+             uv_fs_lutime, *path, atime, mtime);
+    FS_SYNC_TRACE_END(lutimes);
+  }
+}
+
 static void Mkdtemp(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Isolate* isolate = env->isolate();
@@ -2329,6 +2358,7 @@ void Initialize(Local<Object> target,
 
   env->SetMethod(target, "utimes", UTimes);
   env->SetMethod(target, "futimes", FUTimes);
+  env->SetMethod(target, "lutimes", LUTimes);
 
   env->SetMethod(target, "mkdtemp", Mkdtemp);
 
diff --git a/test/parallel/test-fs-utimes.js b/test/parallel/test-fs-utimes.js
index b72d263cf6ce50..b81c5b6bf62940 100644
--- a/test/parallel/test-fs-utimes.js
+++ b/test/parallel/test-fs-utimes.js
@@ -24,13 +24,17 @@ const common = require('../common');
 const assert = require('assert');
 const util = require('util');
 const fs = require('fs');
+const url = require('url');
 
 const tmpdir = require('../common/tmpdir');
 tmpdir.refresh();
 
-function stat_resource(resource) {
+const lpath = `${tmpdir.path}/symlink`;
+fs.symlinkSync('unoent-entry', lpath);
+
+function stat_resource(resource, statSync = fs.statSync) {
   if (typeof resource === 'string') {
-    return fs.statSync(resource);
+    return statSync(resource);
   }
   const stats = fs.fstatSync(resource);
   // Ensure mtime has been written to disk
@@ -41,9 +45,9 @@ function stat_resource(resource) {
   return fs.fstatSync(resource);
 }
 
-function check_mtime(resource, mtime) {
+function check_mtime(resource, mtime, statSync) {
   mtime = fs._toUnixTimestamp(mtime);
-  const stats = stat_resource(resource);
+  const stats = stat_resource(resource, statSync);
   const real_mtime = fs._toUnixTimestamp(stats.mtime);
   return mtime - real_mtime;
 }
@@ -55,8 +59,8 @@ function expect_errno(syscall, resource, err, errno) {
   );
 }
 
-function expect_ok(syscall, resource, err, atime, mtime) {
-  const mtime_diff = check_mtime(resource, mtime);
+function expect_ok(syscall, resource, err, atime, mtime, statSync) {
+  const mtime_diff = check_mtime(resource, mtime, statSync);
   assert(
     // Check up to single-second precision.
     // Sub-second precision is OS and fs dependant.
@@ -68,45 +72,55 @@ function expect_ok(syscall, resource, err, atime, mtime) {
 
 const stats = fs.statSync(tmpdir.path);
 
+const asPath = (path) => path;
+const asUrl = (path) => url.pathToFileURL(path);
+
 const cases = [
-  new Date('1982-09-10 13:37'),
-  new Date(),
-  123456.789,
-  stats.mtime,
-  ['123456', -1],
-  new Date('2017-04-08T17:59:38.008Z')
+  [asPath, new Date('1982-09-10 13:37')],
+  [asPath, new Date()],
+  [asPath, 123456.789],
+  [asPath, stats.mtime],
+  [asPath, '123456', -1],
+  [asPath, new Date('2017-04-08T17:59:38.008Z')],
+  [asUrl, new Date()],
 ];
+
 runTests(cases.values());
 
 function runTests(iter) {
   const { value, done } = iter.next();
   if (done) return;
+
   // Support easy setting same or different atime / mtime values.
-  const [atime, mtime] = Array.isArray(value) ? value : [value, value];
+  const [pathType, atime, mtime = atime] = value;
 
   let fd;
   //
   // test async code paths
   //
-  fs.utimes(tmpdir.path, atime, mtime, common.mustCall((err) => {
+  fs.utimes(pathType(tmpdir.path), atime, mtime, common.mustCall((err) => {
     expect_ok('utimes', tmpdir.path, err, atime, mtime);
 
-    fs.utimes('foobarbaz', atime, mtime, common.mustCall((err) => {
-      expect_errno('utimes', 'foobarbaz', err, 'ENOENT');
+    fs.lutimes(pathType(lpath), atime, mtime, common.mustCall((err) => {
+      expect_ok('lutimes', lpath, err, atime, mtime, fs.lstatSync);
+
+      fs.utimes(pathType('foobarbaz'), atime, mtime, common.mustCall((err) => {
+        expect_errno('utimes', 'foobarbaz', err, 'ENOENT');
 
-      // don't close this fd
-      if (common.isWindows) {
-        fd = fs.openSync(tmpdir.path, 'r+');
-      } else {
-        fd = fs.openSync(tmpdir.path, 'r');
-      }
+        // don't close this fd
+        if (common.isWindows) {
+          fd = fs.openSync(tmpdir.path, 'r+');
+        } else {
+          fd = fs.openSync(tmpdir.path, 'r');
+        }
 
-      fs.futimes(fd, atime, mtime, common.mustCall((err) => {
-        expect_ok('futimes', fd, err, atime, mtime);
+        fs.futimes(fd, atime, mtime, common.mustCall((err) => {
+          expect_ok('futimes', fd, err, atime, mtime);
 
-        syncTests();
+          syncTests();
 
-        setImmediate(common.mustCall(runTests), iter);
+          setImmediate(common.mustCall(runTests), iter);
+        }));
       }));
     }));
   }));
@@ -115,9 +129,12 @@ function runTests(iter) {
   // test synchronized code paths, these functions throw on failure
   //
   function syncTests() {
-    fs.utimesSync(tmpdir.path, atime, mtime);
+    fs.utimesSync(pathType(tmpdir.path), atime, mtime);
     expect_ok('utimesSync', tmpdir.path, undefined, atime, mtime);
 
+    fs.lutimesSync(pathType(lpath), atime, mtime);
+    expect_ok('lutimesSync', lpath, undefined, atime, mtime, fs.lstatSync);
+
     // Some systems don't have futimes
     // if there's an error, it should be ENOSYS
     try {
@@ -129,7 +146,7 @@ function runTests(iter) {
 
     let err;
     try {
-      fs.utimesSync('foobarbaz', atime, mtime);
+      fs.utimesSync(pathType('foobarbaz'), atime, mtime);
     } catch (ex) {
       err = ex;
     }