diff --git a/django/contrib/admin/static/admin/js/SelectBox.js b/django/contrib/admin/static/admin/js/SelectBox.js
index ace6d9dfb8..14c7b8bda7 100644
--- a/django/contrib/admin/static/admin/js/SelectBox.js
+++ b/django/contrib/admin/static/admin/js/SelectBox.js
@@ -1,5 +1,6 @@
 'use strict';
 {
+    const getOptionGroupName = (option) => option.parentElement.label;
     const SelectBox = {
         cache: {},
         init: function(id) {
@@ -7,20 +8,30 @@
             SelectBox.cache[id] = [];
             const cache = SelectBox.cache[id];
             for (const node of box.options) {
-                cache.push({value: node.value, text: node.text, displayed: 1});
+                const group = getOptionGroupName(node);
+                cache.push({group, value: node.value, text: node.text, displayed: 1});
             }
+            SelectBox.sort(id);
         },
         redisplay: function(id) {
             // Repopulate HTML select box from cache
             const box = document.getElementById(id);
             const scroll_value_from_top = box.scrollTop;
             box.innerHTML = '';
-            for (const node of SelectBox.cache[id]) {
-                if (node.displayed) {
-                    const new_option = new Option(node.text, node.value, false, false);
+            let node = box;
+            let group = null;
+            for (const option of SelectBox.cache[id]) {
+                if (option.group && option.group !== group && option.displayed) {
+                    group = option.group;
+                    node = document.createElement('optgroup');
+                    node.setAttribute('label', option.group);
+                    box.appendChild(node);
+                }
+                if (option.displayed) {
+                    const new_option = new Option(option.text, option.value, false, false);
                     // Shows a tooltip when hovering over the option
-                    new_option.title = node.text;
-                    box.appendChild(new_option);
+                    new_option.title = option.text;
+                    node.appendChild(new_option);
                 }
             }
             box.scrollTop = scroll_value_from_top;
@@ -53,7 +64,8 @@
             cache.splice(delete_index, 1);
         },
         add_to_cache: function(id, option) {
-            SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
+            SelectBox.cache[id].push({group: option.group, value: option.value, text: option.text, displayed: 1});
+            SelectBox.sort(id);
         },
         cache_contains: function(id, value) {
             // Check if an item is contained in the cache
@@ -69,7 +81,8 @@
             for (const option of from_box.options) {
                 const option_value = option.value;
                 if (option.selected && SelectBox.cache_contains(from, option_value)) {
-                    SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
+                    const group = getOptionGroupName(option);
+                    SelectBox.add_to_cache(to, {group, value: option_value, text: option.text, displayed: 1});
                     SelectBox.delete_from_cache(from, option_value);
                 }
             }
@@ -81,7 +94,8 @@
             for (const option of from_box.options) {
                 const option_value = option.value;
                 if (SelectBox.cache_contains(from, option_value)) {
-                    SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
+                    const group = getOptionGroupName(option);
+                    SelectBox.add_to_cache(to, {group, value: option_value, text: option.text, displayed: 1});
                     SelectBox.delete_from_cache(from, option_value);
                 }
             }
@@ -90,8 +104,8 @@
         },
         sort: function(id) {
             SelectBox.cache[id].sort(function(a, b) {
-                a = a.text.toLowerCase();
-                b = b.text.toLowerCase();
+                a = (a.group && a.group.toLowerCase() || '') + a.text.toLowerCase();
+                b = (b.group && b.group.toLowerCase() || '') + b.text.toLowerCase();
                 if (a > b) {
                     return 1;
                 }
diff --git a/js_tests/admin/SelectBox.test.js b/js_tests/admin/SelectBox.test.js
index 7d127b5d59..2808dbcacb 100644
--- a/js_tests/admin/SelectBox.test.js
+++ b/js_tests/admin/SelectBox.test.js
@@ -45,3 +45,16 @@ QUnit.test('preserve scroll position', function(assert) {
     assert.equal(toSelectBox.options.length, selectedOptions.length);
     assert.notEqual(fromSelectBox.scrollTop, 0);
 });
+
+QUnit.test('retain optgroups', function(assert) {
+    const $ = django.jQuery;
+    $('<select id="id"></select>').appendTo('#qunit-fixture');
+    const grp = $('<optgroup label="group one">').appendTo('#id');
+    $('<option value="0">A</option>').appendTo(grp);
+    $('</optgroup>').appendTo('#id');
+    $('<option value="1">B</option>').appendTo('#id');
+    SelectBox.init('id');
+    SelectBox.redisplay('id');
+    assert.equal($('#id option').length, 2);
+    assert.equal($('#id optgroup').length, 1);
+});
