diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index c52ee38c39b3bea76a472936389cdbccdb90a090..fbab5df2836d52789db7d19bf405adff89f8381e 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -528,6 +528,16 @@ jquery.once:
     assets/vendor/jquery-once/jquery.once.min.js: { weight: -19, minified: true }
   dependencies:
     - core/jquery
+    - core/jquery.once.bc
+
+jquery.once.bc:
+  version: VERSION
+  js:
+    misc/jquery.once.bc.js: { weight: -19 }
+  dependencies:
+    - core/jquery
+    - core/once
+    - core/drupal.object.assign
 
 jquery.ui:
   version: &jquery_ui_version 1.12.1
diff --git a/core/misc/jquery.once.bc.es6.js b/core/misc/jquery.once.bc.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6fe6bd5aacdc2d9f2be34265978c927eb9ca464
--- /dev/null
+++ b/core/misc/jquery.once.bc.es6.js
@@ -0,0 +1,33 @@
+/**
+ * @file
+ * This file allows calls to `once()` and `once.remove()` to also populate the
+ * jQuery.once registry.
+ *
+ * It allows contributed code still using jQuery.once to behave as expected:
+ * @example
+ * once('core-once-call', 'body');
+ *
+ * // The following will work in a contrib module still using jQuery.once:
+ * $('body').once('core-once-call'); // => returns empty object
+ */
+
+(($, once) => {
+  // We'll replace the whole library so keep a version in cache for later.
+  const drupalOnce = once;
+
+  // When calling once(), also populate jQuery.once registry.
+  function augmentedOnce(id, selector, context) {
+    $(selector, context).once(id);
+    return drupalOnce(id, selector, context);
+  }
+
+  // When calling once.remove(), also remove it from jQuery.once registry.
+  function remove(id, selector, context) {
+    $(selector, context).removeOnce(id);
+    return drupalOnce.remove(id, selector, context);
+  }
+
+  // Expose the rest of @drupal/once API and replace @drupal/once library with
+  // the version augmented with jQuery.once calls.
+  window.once = Object.assign(augmentedOnce, drupalOnce, { remove });
+})(jQuery, once);
diff --git a/core/misc/jquery.once.bc.js b/core/misc/jquery.once.bc.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e7a47597a95458c69681c6abe3e60bf95182128
--- /dev/null
+++ b/core/misc/jquery.once.bc.js
@@ -0,0 +1,24 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, once) {
+  var drupalOnce = once;
+
+  function augmentedOnce(id, selector, context) {
+    $(selector, context).once(id);
+    return drupalOnce(id, selector, context);
+  }
+
+  function remove(id, selector, context) {
+    $(selector, context).removeOnce(id);
+    return drupalOnce.remove(id, selector, context);
+  }
+
+  window.once = Object.assign(augmentedOnce, drupalOnce, {
+    remove: remove
+  });
+})(jQuery, once);
\ No newline at end of file
diff --git a/core/modules/system/tests/modules/js_once_test/js_once_test.info.yml b/core/modules/system/tests/modules/js_once_test/js_once_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c12f79e1f01ead108ea975ff2bf358d7741a7df2
--- /dev/null
+++ b/core/modules/system/tests/modules/js_once_test/js_once_test.info.yml
@@ -0,0 +1,5 @@
+name: 'JS once Test'
+type: module
+description: 'Module for the jsOnceTest.'
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/js_once_test/js_once_test.routing.yml b/core/modules/system/tests/modules/js_once_test/js_once_test.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c73a876b3ae6a7de9ffa01d3aa8c20c8a224822f
--- /dev/null
+++ b/core/modules/system/tests/modules/js_once_test/js_once_test.routing.yml
@@ -0,0 +1,14 @@
+js_once_test:
+  path: '/js_once_test'
+  defaults:
+    _controller: '\Drupal\js_once_test\Controller\JsOnceTestController::onceTest'
+    _title: 'OnceTest'
+  requirements:
+    _access: 'TRUE'
+js_once_test.with_bc:
+  path: '/js_once_with_bc_test'
+  defaults:
+    _controller: '\Drupal\js_once_test\Controller\JsOnceTestController::onceBcTest'
+    _title: 'OnceBcTest'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/js_once_test/src/Controller/JsOnceTestController.php b/core/modules/system/tests/modules/js_once_test/src/Controller/JsOnceTestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..c47505cd3bba5dc382511b7c4892f98f0198dec5
--- /dev/null
+++ b/core/modules/system/tests/modules/js_once_test/src/Controller/JsOnceTestController.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\js_once_test\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Controller for testing the @drupal/once library integration.
+ */
+class JsOnceTestController extends ControllerBase {
+
+  /**
+   * Provides elements for testing @drupal/once.
+   *
+   * @return array
+   *   The render array.
+   */
+  public function onceTest() {
+    $output = [
+      '#attached' => ['library' => ['core/once']],
+    ];
+    foreach (range(1, 5) as $item) {
+      $output['item' . $item] = [
+        '#type' => 'html_tag',
+        '#tag' => 'div',
+        '#value' => 'Item ' . $item,
+        '#attributes' => [
+          'data-drupal-item' => $item,
+        ],
+      ];
+    }
+    return $output;
+  }
+
+  /**
+   * Provides elements for testing jQuery Once BC support.
+   *
+   * @return array
+   *   The render array.
+   */
+  public function onceBcTest() {
+    $output = [
+      '#attached' => ['library' => ['core/jquery.once']],
+    ];
+    foreach (range(1, 5) as $item) {
+      $output['item' . $item] = [
+        '#type' => 'html_tag',
+        '#tag' => 'div',
+        '#value' => 'Item ' . $item,
+        '#attributes' => [
+          'data-drupal-item' => $item,
+        ],
+      ];
+    }
+    return $output;
+  }
+
+}
diff --git a/core/tests/Drupal/Nightwatch/Tests/jsOnceTest.js b/core/tests/Drupal/Nightwatch/Tests/jsOnceTest.js
new file mode 100644
index 0000000000000000000000000000000000000000..b1f4a715352ddcab9f4e0dad7d9a426a24d17f7c
--- /dev/null
+++ b/core/tests/Drupal/Nightwatch/Tests/jsOnceTest.js
@@ -0,0 +1,210 @@
+module.exports = {
+  '@tags': ['core'],
+  before(browser) {
+    browser.drupalInstall().drupalLoginAsAdmin(() => {
+      browser
+        .drupalRelativeURL('/admin/modules')
+        .setValue('input[type="search"]', 'JS Once Test')
+        .waitForElementVisible(
+          'input[name="modules[js_once_test][enable]"]',
+          1000,
+        )
+        .click('input[name="modules[js_once_test][enable]"]')
+        .click('input[type="submit"]'); // Submit module form.
+    });
+  },
+  after(browser) {
+    browser.drupalUninstall();
+  },
+  'Test simple once call': (browser) => {
+    browser
+      .drupalRelativeURL('/js_once_test')
+      .waitForElementVisible('[data-drupal-item]', 1000)
+      // prettier-ignore
+      .execute(
+        function () {
+          return once('js_once_test', '[data-drupal-item]');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            5,
+            '5 items returned and "once-d"',
+          );
+        },
+      )
+      // Check that follow-up calls to once return an empty array.
+      .execute(
+        function () {
+          return once('js_once_test', '[data-drupal-item]');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            0,
+            '0 items returned',
+          );
+        },
+      )
+      .execute(
+        function () {
+          return once(
+            'js_once_test_extra',
+            '[data-drupal-item="1"],[data-drupal-item="2"]',
+          );
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            2,
+            '2 items returned and "once-d"',
+          );
+        },
+      )
+      .execute(
+        function () {
+          return once(
+            'js_once_test_extra',
+            '[data-drupal-item="1"],[data-drupal-item="2"]',
+          );
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            0,
+            '0 items returned',
+          );
+        },
+      )
+      .execute(
+        function () {
+          return once.remove('js_once_test', '[data-drupal-item]');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            5,
+            '5 items returned and "de-once-d"',
+          );
+        },
+      )
+      .execute(
+        function () {
+          return once.remove('js_once_test', '[data-drupal-item]');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            0,
+            '0 items returned',
+          );
+        },
+      )
+      .execute(
+        function () {
+          return once.remove(
+            'js_once_test_extra',
+            '[data-drupal-item="1"],[data-drupal-item="2"]',
+          );
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            2,
+            '2 items returned and "de-once-d"',
+          );
+        },
+      )
+      .execute(
+        function () {
+          return once.remove(
+            'js_once_test_extra',
+            '[data-drupal-item="1"],[data-drupal-item="2"]',
+          );
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            0,
+            '0 items returned',
+          );
+        },
+      )
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test BC layer with jQuery Once calls': (browser) => {
+    browser
+      .drupalRelativeURL('/js_once_with_bc_test')
+      .waitForElementVisible('[data-drupal-item]', 1000)
+      // prettier-ignore
+      .execute(
+        function () {
+          // A core script calls once on some elements.
+          once('js_once_test', '[data-drupal-item]');
+          // A contrib module not yet using @drupal/once calls jQuery Once.
+          return jQuery('[data-drupal-item]').once('js_once_test');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            0,
+            'Calls to once() are taken into account when using jQuery.once()',
+          );
+        },
+      )
+      // Once calls don't take into account calls to jQuery.once by design.
+      .execute(
+        function () {
+          // Calling jQuery.once before @drupal/once will lead to duplicate
+          // processing.
+          jQuery('[data-drupal-item]').once('js_once_test_extra');
+          // A core script calls once on some elements.
+          return once('js_once_test_extra', '[data-drupal-item]');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            5,
+            '5 items returned by once() after a call to jQuery.once()',
+          );
+        },
+      )
+      .execute(
+        function () {
+          once('js_once_test_remove', '[data-drupal-item]');
+          // A core script calls once on some elements.
+          once.remove('js_once_test_remove', '[data-drupal-item]');
+          // A contrib module not yet using @drupal/once calls the jQuery Once
+          // remove() function.
+          return jQuery('[data-drupal-item]').removeOnce('js_once_test_remove');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            0,
+            'Calls to once.remove() are taken into account when using jQuery.removeOnce()',
+          );
+        },
+      )
+      // Once.remove calls don't take into account calls to jQuery.removeOnce by
+      // design.
+      .execute(
+        function () {
+          once('js_once_test_remove_fail', '[data-drupal-item]');
+          // Calling jQuery.removeOnce before @drupal/once will lead to
+          // duplicate processing.
+          jQuery('[data-drupal-item]').removeOnce('js_once_test_remove_fail');
+          // A core script calls once.remove on some elements.
+          return once.remove('js_once_test_remove_fail', '[data-drupal-item]');
+        },
+        (result) => {
+          browser.assert.strictEqual(
+            result.value.length,
+            5,
+            '5 items returned by once.remove() after a call to jQuery.removeOnce()',
+          );
+        },
+      )
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+};