diff --git a/pyupgrade.py b/pyupgrade.py
index 8e1fb1c..f675fc1 100644
--- a/pyupgrade.py
+++ b/pyupgrade.py
@@ -1225,6 +1225,7 @@ class FindPy3Plus(ast.NodeVisitor):
             set,
         )  # type: Dict[str, Set[str]]
         self.io_open_calls = set()  # type: Set[Offset]
+        self.open_mode_calls = set()  # type: Set[Offset]
         self.os_error_alias_calls = set()  # type: Set[Offset]
         self.os_error_alias_simple = {}  # type: Dict[Offset, NameOrAttr]
         self.os_error_alias_excepts = set()  # type: Set[Offset]
@@ -1500,6 +1501,15 @@ class FindPy3Plus(ast.NodeVisitor):
             self.encode_calls[_ast_to_offset(node)] = node
         elif self._is_io_open(node.func):
             self.io_open_calls.add(_ast_to_offset(node))
+        elif (
+                isinstance(node.func, ast.Name) and
+                node.func.id == 'open' and
+                len(node.args) >= 2 and
+                not _starargs(node) and
+                isinstance(node.args[1], ast.Str) and
+                node.args[1].s in {'Ur', 'rU', 'Ub', 'bU', 'r', 'rt', 'tr'}
+        ):
+            self.open_mode_calls.add(_ast_to_offset(node))
 
         self.generic_visit(node)
 
@@ -1882,6 +1892,7 @@ def _fix_py3_plus(contents_text):  # type: (str) -> str
             visitor.if_py3_blocks_else,
             visitor.native_literals,
             visitor.io_open_calls,
+            visitor.open_mode_calls,
             visitor.os_error_alias_calls,
             visitor.os_error_alias_simple,
             visitor.os_error_alias_excepts,
@@ -2061,6 +2072,18 @@ def _fix_py3_plus(contents_text):  # type: (str) -> str
         elif token.offset in visitor.io_open_calls:
             j = _find_open_paren(tokens, i)
             tokens[i:j] = [token._replace(name='NAME', src='open')]
+        elif token.offset in visitor.open_mode_calls:
+            j = _find_open_paren(tokens, i)
+            func_args, end = _parse_call_args(tokens, j)
+            mode = tokens_to_src(tokens[slice(*func_args[1])])
+            mode_stripped = mode.strip().strip('"\'')
+            if mode_stripped in {'Ur', 'rU', 'r', 'rt'}:
+                del tokens[func_args[0][1]:func_args[1][1]]
+            elif mode_stripped in {'Ub', 'bU'}:
+                new_mode = mode.replace('U', 'r')
+                tokens[slice(*func_args[1])] = [Token('SRC', new_mode)]
+            else:
+                raise AssertionError('unreachable: {!r}'.format(mode))
         elif token.offset in visitor.os_error_alias_calls:
             j = _find_open_paren(tokens, i)
             tokens[i:j] = [token._replace(name='NAME', src='OSError')]

Replace open(file, "rU") with open(file)?
"U" (universal newlines) `mode` was deprecated in Python 3.4 and the `newline=None` argument handles it instead. The default mode is `"r"` anyway
diff --git a/tests/open_mode_test.py b/tests/open_mode_test.py
new file mode 100644
index 0000000..dd6f939
--- /dev/null
+++ b/tests/open_mode_test.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import pytest
+
+from pyupgrade import _fix_py3_plus
+
+
+@pytest.mark.parametrize(
+    's',
+    (
+        # already a reduced mode
+        'open("foo", "w")',
+        'open("foo", "rb")',
+        # nonsense mode
+        'open("foo", "Uw")',
+        # TODO: could maybe be rewritten to remove t?
+        'open("foo", "wt")',
+    ),
+)
+def test_fix_open_mode_noop(s):
+    assert _fix_py3_plus(s) == s
+
+
+@pytest.mark.parametrize(
+    ('s', 'expected'),
+    (
+        ('open("foo", "Ur")', 'open("foo")'),
+        ('open("foo", "Ub")', 'open("foo", "rb")'),
+        ('open("foo", "r")', 'open("foo")'),
+        ('open("foo", "rt")', 'open("foo")'),
+        ('open("f", "r", encoding="UTF-8")', 'open("f", encoding="UTF-8")'),
+    ),
+)
+def test_fix_open_mode(s, expected):
+    assert _fix_py3_plus(s) == expected

pip install -e .
parse_log_pytest
pytest
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
-e git+https://github.com/asottile/pyupgrade.git@93a8c0f191e3ff269f5824bf1010f2920f0b1284#egg=pyupgrade
tokenize_rt==6.1.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: pyupgrade
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - tokenize-rt==6.1.0
prefix: /opt/conda/envs/pyupgrade

tests/open_mode_test.py::test_fix_open_mode[open("foo", tests/open_mode_test.py::test_fix_open_mode[open("f",
tests/open_mode_test.py::test_fix_open_mode_noop[open("foo",
pyupgrade.py
pyupgrade.py