diff --git a/CHANGES.rst b/CHANGES.rst
index 279fd3f7d..367cfb668 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -7,6 +7,9 @@ Unreleased
 
 -   Make reloader more robust when ``""`` is in ``sys.path``. :pr:`2823`
 -   Better TLS cert format with ``adhoc`` dev certs. :pr:`2891`
+-   Inform Python < 3.12 how to handle ``itms-services`` URIs correctly, rather
+    than using an overly-broad workaround in Werkzeug that caused some redirect
+    URIs to be passed on without encoding. :issue:`2828`
 
 
 Version 3.0.2
diff --git a/src/werkzeug/urls.py b/src/werkzeug/urls.py
index 4d61e600b..5bffe3928 100644
--- a/src/werkzeug/urls.py
+++ b/src/werkzeug/urls.py
@@ -3,6 +3,7 @@
 import codecs
 import re
 import typing as t
+import urllib.parse
 from urllib.parse import quote
 from urllib.parse import unquote
 from urllib.parse import urlencode
@@ -164,25 +165,11 @@ def iri_to_uri(iri: str) -> str:
     return urlunsplit((parts.scheme, netloc, path, query, fragment))
 
 
-def _invalid_iri_to_uri(iri: str) -> str:
-    """The URL scheme ``itms-services://`` must contain the ``//`` even though it does
-    not have a host component. There may be other invalid schemes as well. Currently,
-    responses will always call ``iri_to_uri`` on the redirect ``Location`` header, which
-    removes the ``//``. For now, if the IRI only contains ASCII and does not contain
-    spaces, pass it on as-is. In Werkzeug 3.0, this should become a
-    ``response.process_location`` flag.
-
-    :meta private:
-    """
-    try:
-        iri.encode("ascii")
-    except UnicodeError:
-        pass
-    else:
-        if len(iri.split(None, 1)) == 1:
-            return iri
-
-    return iri_to_uri(iri)
+# Python < 3.12
+# itms-services was worked around in previous iri_to_uri implementations, but
+# we can tell Python directly that it needs to preserve the //.
+if "itms-services" not in urllib.parse.uses_netloc:
+    urllib.parse.uses_netloc.append("itms-services")
 
 
 def _decode_idna(domain: str) -> str:
diff --git a/src/werkzeug/wrappers/response.py b/src/werkzeug/wrappers/response.py
index 7b666e3e8..7f01287c7 100644
--- a/src/werkzeug/wrappers/response.py
+++ b/src/werkzeug/wrappers/response.py
@@ -14,7 +14,6 @@
 from ..http import parse_range_header
 from ..http import remove_entity_headers
 from ..sansio.response import Response as _SansIOResponse
-from ..urls import _invalid_iri_to_uri
 from ..urls import iri_to_uri
 from ..utils import cached_property
 from ..wsgi import _RangeWrapper
@@ -479,7 +478,7 @@ def get_wsgi_headers(self, environ: WSGIEnvironment) -> Headers:
                 content_length = value
 
         if location is not None:
-            location = _invalid_iri_to_uri(location)
+            location = iri_to_uri(location)
 
             if self.autocorrect_location_header:
                 # Make the location header an absolute URL.
diff --git a/tests/test_urls.py b/tests/test_urls.py
index fdaa913a6..101b886ec 100644
--- a/tests/test_urls.py
+++ b/tests/test_urls.py
@@ -98,3 +98,9 @@ def test_iri_to_uri_dont_quote_valid_code_points():
     # [] are not valid URL code points according to WhatWG URL Standard
     # https://url.spec.whatwg.org/#url-code-points
     assert urls.iri_to_uri("/path[bracket]?(paren)") == "/path%5Bbracket%5D?(paren)"
+
+
+# Python < 3.12
+def test_itms_services() -> None:
+    url = "itms-services://?action=download-manifest&url=https://test.example/path"
+    assert urls.iri_to_uri(url) == url
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
index d7bc12b95..f75694459 100644
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -1154,6 +1154,7 @@ class MyResponse(wrappers.Response):
     ("auto", "location", "expect"),
     (
         (False, "/test", "/test"),
+        (False, "/\\\\test.example?q", "/%5C%5Ctest.example?q"),
         (True, "/test", "http://localhost/test"),
         (True, "test", "http://localhost/a/b/test"),
         (True, "./test", "http://localhost/a/b/test"),