Do not add an extra blank line to an import line that has fmt disabled
diff --git a/CHANGES.md b/CHANGES.md
index eff2640a01e..06a0ab7e9eb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,9 @@
 
 <!-- Changes that affect Black's stable style -->
 
+- Import lines with `# fmt: skip` and `# fmt: off` no longer have an extra blank line
+  added when they are right after another import line (#3610)
+
 ### Preview style
 
 <!-- Changes that affect Black's preview style -->
diff --git a/src/black/comments.py b/src/black/comments.py
index 7cf15bf67b3..619123ab4be 100644
--- a/src/black/comments.py
+++ b/src/black/comments.py
@@ -203,6 +203,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
                     STANDALONE_COMMENT,
                     hidden_value,
                     prefix=standalone_comment_prefix,
+                    fmt_pass_converted_first_leaf=first_leaf_of(first),
                 ),
             )
             return True
diff --git a/src/black/lines.py b/src/black/lines.py
index 329dfc4f0d3..66bba14b357 100644
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -195,6 +195,26 @@ def opens_block(self) -> bool:
             return False
         return self.leaves[-1].type == token.COLON
 
+    def is_fmt_pass_converted(
+        self, *, first_leaf_matches: Optional[Callable[[Leaf], bool]] = None
+    ) -> bool:
+        """Is this line converted from fmt off/skip code?
+
+        If first_leaf_matches is not None, it only returns True if the first
+        leaf of converted code matches.
+        """
+        if len(self.leaves) != 1:
+            return False
+        leaf = self.leaves[0]
+        if (
+            leaf.type != STANDALONE_COMMENT
+            or leaf.fmt_pass_converted_first_leaf is None
+        ):
+            return False
+        return first_leaf_matches is None or first_leaf_matches(
+            leaf.fmt_pass_converted_first_leaf
+        )
+
     def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool:
         """If so, needs to be split before emitting."""
         for leaf in self.leaves:
@@ -597,6 +617,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
             self.previous_line
             and self.previous_line.is_import
             and not current_line.is_import
+            and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import)
             and depth == self.previous_line.depth
         ):
             return (before or 1), 0
diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py
index 15a1420ef7d..ea60c894e20 100644
--- a/src/blib2to3/pytree.py
+++ b/src/blib2to3/pytree.py
@@ -392,6 +392,10 @@ class Leaf(Base):
     _prefix = ""  # Whitespace and comments preceding this token in the input
     lineno: int = 0  # Line where this token starts in the input
     column: int = 0  # Column where this token starts in the input
+    # If not None, this Leaf is created by converting a block of fmt off/skip
+    # code, and `fmt_pass_converted_first_leaf` points to the first Leaf in the
+    # converted code.
+    fmt_pass_converted_first_leaf: Optional["Leaf"] = None
 
     def __init__(
         self,
@@ -401,6 +405,7 @@ def __init__(
         prefix: Optional[Text] = None,
         fixers_applied: List[Any] = [],
         opening_bracket: Optional["Leaf"] = None,
+        fmt_pass_converted_first_leaf: Optional["Leaf"] = None,
     ) -> None:
         """
         Initializer.
@@ -419,6 +424,7 @@ def __init__(
         self.fixers_applied: Optional[List[Any]] = fixers_applied[:]
         self.children = []
         self.opening_bracket = opening_bracket
+        self.fmt_pass_converted_first_leaf = fmt_pass_converted_first_leaf
 
     def __repr__(self) -> str:
         """Return a canonical string representation."""
diff --git a/tests/data/simple_cases/fmtonoff.py b/tests/data/simple_cases/fmtonoff.py
index e40ea2c8d21..d1f15cd5c8b 100644
--- a/tests/data/simple_cases/fmtonoff.py
+++ b/tests/data/simple_cases/fmtonoff.py
@@ -195,7 +195,6 @@ def single_literal_yapf_disable():
 from third_party import X, Y, Z
 
 from library import some_connection, some_decorator
-
 # fmt: off
 from third_party import (X,
                          Y, Z)
diff --git a/tests/data/simple_cases/fmtpass_imports.py b/tests/data/simple_cases/fmtpass_imports.py
new file mode 100644
index 00000000000..8b3c0bc662a
--- /dev/null
+++ b/tests/data/simple_cases/fmtpass_imports.py
@@ -0,0 +1,19 @@
+# Regression test for https://github.com/psf/black/issues/3438
+
+import ast
+import collections  # fmt: skip
+import dataclasses
+# fmt: off
+import os
+# fmt: on
+import pathlib
+
+import re  # fmt: skip
+import secrets
+
+# fmt: off
+import sys
+# fmt: on
+
+import tempfile
+import zoneinfo

<!-- Hello! Thanks for submitting a PR. To help make things go a bit more
     smoothly we would appreciate that you go through this template. -->

### Description

Fixes #3438.

This isn't put in preview style because:

1. Already formatted code always has the extra blank line, so it shouldn't change already formatted code and doesn't violate the stability policy.
2. This unblocks us from upstreaming the incremental formatting patches in *Pyink*, and we don't want to require `--preview` (in at least in 2023) to use this feature. It would also make integrations with IDEs / pre-commit harder if we require `--preview`.

<!-- Good things to put here include: reasoning for the change (please link
     any relevant issues!), any noteworthy (or hacky) choices to be aware of,
     or what the problem resolved here looked like ... we won't mind a ranty
     story :) -->

### Checklist - did you ...

<!-- If any of the following items aren't relevant for your contribution
     please still tick them so we know you've gone through the checklist.

    All user-facing changes should get an entry. Otherwise, signal to us
    this should get the magical label to silence the CHANGELOG entry check.
    Tests are required for bugfixes and new features. Documentation changes
    are necessary for formatting and most enhancement changes. -->

- [x] Add an entry in `CHANGES.md` if necessary?
- [x] Add / update tests if necessary?
- [x] Add new / update outdated documentation?

<!-- Just as a reminder, everyone in all psf/black spaces including PRs
     must follow the PSF Code of Conduct (link below).

     Finally, once again thanks for your time and effort. If you have any
     feedback in regards to your experience contributing here, please
     let us know!

     Helpful links:

      PSF COC: https://www.python.org/psf/conduct/
      Contributing docs: https://black.readthedocs.io/en/latest/contributing/index.html
      Chat on Python Discord: https://discord.gg/RtVdv86PrH -->
